A Hands-on Guide: Building and Deploying Containerized web application with ZK and AWS Lightsail

From Documentation
DocumentationSmall Talks2023MayA Hands-on Guide: Building and Deploying Containerized web application with ZK and AWS Lightsail
A Hands-on Guide: Building and Deploying Containerized web application with ZK and AWS Lightsail

Author
Hawk Chen, Engineer, Potix Corporation
Date
May. 31, 2023
Version
ZK 10FL

Overview

In this article, I will delve into the process of building and deploying a cloud-based web application using the ZK 10 and AWS Lightsail. ZK 10, currently under development, introduces an exciting feature, stateless components, designed to aid developers in creating efficient, cloud-based web applications. This makes enhancing scalability, high availability, resource management, and auto-provisioning much easier.

This article will cover stateless components' basics, configuration, and usage. We will also guide you through the steps of deploying your application using Docker containers on AWS Lightsail.

Basic Concept of Stateless Components

The Differences Between Stateless and Stateful Components

ZK is a server-centric framework, and the standard UI components we have been offering are stateful(ZK 9 and before). Every UI component produces a Java object and a JavaScript widget that maintain their state in synchrony. The desktop and its component tree are stored in an HTTP session. This model, while powerful, required memory on the server to maintain the state of each Java object.

However, with the advent of ZK 10, a new type of component is introduced: the stateless component. In contrast to standard components, stateless components do not have a corresponding Java object keeping their state and no persistent desktop. Instead, only JavaScript widgets maintain the state of the UI. This means ZK no longer requires requests in the same session to be sent to the same server. They can be processed from any clustered node so that you can fully leverage cloud resources in a more dynamic and resilient manner and achieve autoscaling and high availability much easier. Another significant advantage of this architecture is that stateless components do not consume server memory, as no Java objects hold their state.

The screenshot below demonstrates that no component (Java) objects are created when using stateless components:

Heap dump in VisualVM

NoComponentObject.png

Configure Dispatcher Richlet Filter

To start working with stateless components, you'll need to configure the DispatcherRichletFilter in your web.xml file. The DispatcherRichletFilter is a core role in handling requests in ZK 10. It maps incoming HTTP requests to methods in a StatelessRichlet that are annotated with the @RichletMapping, based on the URL pattern defined in the annotation.

Here is a basic configuration of the DispatcherRichletFilter:

<filter>
		<filter-name>DispatcherRichletFilter</filter-name>
		<filter-class>org.zkoss.stateless.ui.http.DispatcherRichletFilter</filter-class>
		<init-param>
			<param-name>basePackages</param-name>
			<param-value>org.zkoss.stateless.demo</param-value>
		</init-param>
		<init-param>
			<param-name>cloudMode</param-name>
			<param-value>true</param-value>
		</init-param>
</filter>
<filter-mapping>
		<filter-name>DispatcherRichletFilter</filter-name>
		<url-pattern>/*</url-pattern>
</filter-mapping>
  • The basePackages parameter in the configuration specifies the packages that the filter should scan for @RichletMapping. This is a required parameter that you should specify according to your conditions.


Building UI with Stateless Components

In ZK 10, to build a UI with stateless components, one needs to create a class that implements the StatelessRichlet interface and designates its access URL. This can be done using the @RichletMapping on both the class and method to define the access URL by concatenating class and method annotation values:

[CLASS_VALUE][METHOD_VALUE]
@RichletMapping("/shoppingCart")
public class ShoppingCartRichlet implements StatelessRichlet {

	@RichletMapping("")
	public List<IComponent> index() {
		//... your code here
	}

Based on the code above, the access URL is /shoppingcart

The new stateless component API introduces a fluent API style, where each class begins with an uppercase "I", indicating "Immutable". Below is an example of how to build a UI with this new API.


	@RichletMapping("")
	public List<IComponent> index() {
		return asList(
			IStyle.ofSrc(DEMO_CSS),
			IVlayout.of(
				renderShoppingBag(),
				Boilerplate.ORDER_TEMPLATE
			)
		);
	}
	private IVlayout renderShoppingBag() {
		final String orderId = Helper.nextUuid();
		return IVlayout.of(
			ILabel.of("Shopping Cart").withSclass("title"),
			renderOrderButtons(orderId),
			IGrid.ofId("shoppingBag").withHflex("1")
				.withEmptyMessage("please add items.")
				.withColumns(Boilerplate.SHOPPING_BAG_COLUMN_TEMPLATE)
				.withRows(renderShoppingBagItems(orderId))
				)
		.withSclass("shoppingBag");
	}
  • of() is used to append child components
  • withAttr() is used to set a component's attributes. For example, withId() is used to set the ID of the component.

Action Handling

In the new ZK 10's stateless component model, the traditional "event listeners" from ZK 9 are replaced with "action handlers". You can set an action handler with the withAction() , such as IButton.of("add item +").withAction(this::addItem).

To make a method an action handler, you apply the @Action with an event type to listen to. An example is shown below:

	@Action(type = Events.ON_CLICK)
	public void addItem() {
		//... your code here
	}

In the code snippet above, the @Action(type = Events.ON_CLICK) denotes that the method addItem() is an action handler for the ON_CLICK event type. When a click event occurs, this addItem() method will be invoked to handle the action.


Obtain Component State

Given the stateless nature of ZK 10's components, obtaining the component's state from the server using a getter is no longer possible. Instead, you need to obtain the states from a browser. ZK 10 provides the @ActionVariable to access component states sent from a browser.

@Action(type = Events.ON_CLICK)
public void addItem(@ActionVariable(targetId = ActionTarget.SELF, field = "id") String id) {
		// your code here
}
  • ActionTarget.SELF refers to the event target, which in this case is the “add item” button. This could also be ActionTarget.PARENT, ActionTarget.FIRST_CHILD, ActionTarget.LAST_CHILD, and so forth.

Here is another example:

@Action(type = Events.ON_CHANGE)
public void changeQuantity(Self self,
			   InputData quantityData,
			   @ActionVariable(targetId = ActionTarget.NEXT_SIBLING) Integer price,
			   @ActionVariable(targetId = ActionTarget.PARENT, field = "id") String id) {
	// your code here
}
  • self refers to the locator of the component itself
  • quantityData represents input data caused by a user's input something at the client.

Update Component State

Since the server no longer holds components in ZK 10, a UiAgent is used to control the client-side widgets and allow them to update themselves.

	@Action(type = Events.ON_CLICK)
	public void deleteItem(Self self, @ActionVariable(targetId = ActionTarget.PARENT, field = "id") String id) {
		orderService.delete(parseItemId(id));
		UiAgent.getCurrent().remove(Locator.ofId(id));
		// your code here
	}
  • The Locator.ofId() function returns a locator to the given ID of an `IComponent`. This is used to specify the component to be removed by the `UiAgent`.

Deploying the App

Now that we have a simple web application created using ZK 10 stateless components, we can either deploy it to a standalone web server just like we used to do or deploy it to a cloud environment so that we can fully leverage the flexible scaling and auto-provisioning features.

You are free to choose the cloud platform you wish to deploy your app to. In this article, we will take AWS Lightsail as an example to demonstrate how the process is done.

AWS Lightsail Getting Started

Amazon Web Services Lightsail is a simple yet powerful platform that allows you to build, deploy, and manage your web applications effortlessly. It's designed to be easy to use, and it's ideal for beginners who want to quickly get a project off the ground. For a more detailed introduction and guide to using Lightsail, I recommend referencing the official AWS tutorial: "a Container Web App on Amazon Lightsail".

Common uses for Lightsail include:

  • Web Application Hosting
  • Dev/Test Environments
  • Microservices
  • Content Delivery and Media Serving


Install the AWS CLI

To interact with AWS services, you'll need the AWS Command Line Interface (CLI) installed on your local machine. Follow the official instructions provided to get started.

Configure AWS CLI Credentials

After installing the AWS CLI, the next step is to configure your credentials. This will allow the CLI to authenticate your requests to AWS. Again, follow the official guide to set up your credentials. Details will not be covered here as the AWS guide provides comprehensive instructions.

Prepare Docker Image

To get your web application running on Lightsail, you'll need to package it as a Docker image. We assume you have Docker Desktop installed on your machine and are familiar with basic Docker commands. In this guide, our Dockerfile will be based on openjdk:8-jdk, and we'll also install postgresql-12 and Tomcat 8 to run the war file.

Steps to run a container service

  1. Create a Container Service
  2. Push a Local Image
  3. Deploy That Image to the Container Service

Create a Container Service

An Amazon Lightsail container service is a compute resource where you can deploy your Docker images. Think of your Lightsail container service as a computing environment that allows you to run containers on AWS infrastructure using images that you create.

To get started with Lightsail container services, you will need to install the AWS CLI Lightsail Extension. Follow the official instructions provided by Amazon.

A container service is comprised of compute nodes, a TLS certificate, a DNS domain name, and an optional load balancer. For simplicity, a shell script named create-container.sh is provided to create a container service. After executing the script, wait for a while until the container service's status changes to READY.

Container.png

Push a Local Image

Amazon Lightsail supports the deployment of containers from various public container image repositories, such as Docker Hub, Amazon ECR Public Gallery, and even your local machine.

To begin, build a Docker image using docker-build.sh. You can then test the Docker image locally by running docker-up.sh. To push the image from your local machine to Lightsail, execute push-container.sh. The pushed image, in this case, named 'shoppingcart', will be stored on Amazon Lightsail and can be verified under the "Image" tab in your container service.

ContainerImages.png

Deploy That Image to the Container Service

After the image is pushed, it needs to be deployed to the container service to run. This can be achieved by executing the script deploy-container.sh or through the Lightsail console.

Once the deployment process is complete and the container service's status changes to RUNNING, you can visit the public domain assigned to your service to access the application.

Deployment.png

Change Capacity to Serve More Requests

After deploying to a container service, we can easily add running nodes to serve more users(requests). Lightsail does not natively support auto-scaling features like other AWS services, such as EC2 or Fargate. But you can change capacity manually:

ChangeCapacity.png

You can change

  • The power of each node
  • The number of each node

(surely that increasing power costs more money)

I send 100 requests in 5 seconds with Jmeter to the application running in 1 node and 3 nodes, you can see the response time is obviously shorter for 3 nodes:

# of Nodes # of HTTP Requests Average Response Time Min Max Std. Dev.
1 100 3632 517 6417 1737.77
3 100 1407 219 3138 724.16


Identifying Individual Nodes with a Unique Application ID

After creating 3 nodes, each time you visit the public URL, you might be directed to one of the nodes. Normally, you can't differentiate these 3 nodes. If I generate an application ID and show it on a page, you can find that each page reloading might take you to a different node.

3nodes.png

Troubleshooting

Deployment failed for exec format error

⁉️ Error in the server log

exec /docker-entrypoint.sh: exec format error

This error usually indicates that the file you're trying to execute is not in the correct format. In this case, it could mean that the entry point script you're trying to run in the Docker image (/docker-entrypoint.sh) is not in the correct format for the underlying system architecture.

Solution

Make sure the Docker image you're using is compatible with the system architecture of the Fargate cluster. If you're using a 64-bit Ubuntu 18.04 image, for example, the architecture should be x86_64.

Modify Dockerfile:

FROM amd64/nginx:1.23.3

⁉️ Exec format error

Error in the server log

[16/May/2023:08:37:49] [deployment:1] Creating your deployment
[16/May/2023:08:38:18] exec /start.sh: exec format error
[16/May/2023:08:39:28] [deployment:1] Started 1 new node
…
[16/May/2023:10:36:54] [deployment:2] Took too long

Root Cause

PostgreSQL is also architecture dependent.

Solution

Specify PostgreSQL with amd64 architecture:

RUN echo "deb [arch=amd64] http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" > /etc/apt/sources.list.d/pgdg.list

⁉️ Health checks failed: port 8080 is unhealthy

Error in the server log

[17/May/2023:07:51:30] [deployment:7] Health checks failed: port 8080 is unhealthy

Root cause

The default health check timeout of 2s is too short for my application.

Solution

Increase the timeout and interval and redeploy. Change the setting to 10s via the Lightsail console.

Source Code

check the source code


Comments



Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.